#!/usr/bin/env python

import os, sys, re, signal
from socket import gethostname

try:
    import avahi, gtk, gtk.glade, gobject, dbus, avahi.ServiceTypeDatabase, pynotify, gconf
    pynotify.init('Synergy wrapper')
except ImportError, e:
    print "Sorry, to use this tool you need to install Avahi, pygtk and python-dbus.\n Error: %s" % e
    sys.exit(1)

try:
    from dbus.dbus_bindings import DBusException
    import dbus.glib
except ImportError, e:
    pass

COLUMN_HOST = 0
COLUMN_ACTIVE = 1
COLUMN_LABEL = 2
COLUMN_ONLINE = 3

GCONF_KEY = '/apps/synergy/managed_screens'

def error_msg(msg):
    d = gtk.MessageDialog(parent = None, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_ERROR, buttons = gtk.BUTTONS_OK)
    d.set_markup(msg)
    d.show_all()
    d.run()
    d.destroy()

class MainWindow:
    glade = None
    mainWindow = None
    treeView = None
    listStore = None
    discovered = {}
    nameToHost = {}
    parent = None
    gconf = None

    def __init__(self, parent, **kwargs):
        self.parent = parent
        self.bus = parent.bus
        self.server = parent.server
        self.glade = gtk.glade.XML('synergys.glade', 'main_window', None)
        self.mainWindow = self.glade.get_widget('main_window')
        self.listStore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
        self.treeView = self.glade.get_widget('machineList')
        self.treeView.set_model(self.listStore)
        renderer = gtk.CellRendererToggle()
        renderer.set_property('activatable', True)
        renderer.connect('toggled', self.computer_toggled, (self.listStore, COLUMN_ACTIVE))
        column = gtk.TreeViewColumn('Active', renderer, active = COLUMN_ACTIVE)
        self.treeView.append_column(column)
        renderer = gtk.CellRendererPixbuf()
        column = gtk.TreeViewColumn('Online', renderer)
        column.set_cell_data_func(renderer, self.render_icon_for_computer)
        self.treeView.append_column(column)
        column = gtk.TreeViewColumn('Host', gtk.CellRendererText(), markup = COLUMN_LABEL)
        self.treeView.append_column(column)
        self.load_config()
        self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 'local')
        self.glade.signal_autoconnect(self)
        self.listStore.connect('rows-reordered', self.rows_reordered)
        self.mainWindow.show_all()

    def load_config(self):
        self.gconf = gconf.client_get_default()
        hosts = self.gconf.get_list(GCONF_KEY, gconf.VALUE_STRING)
        for host in hosts:
            self.host_found(host, '', True, False)

    def render_icon_for_computer(self, column, cell, model, iter, user_data = None):
        icon = 'network-idle'
        if not model.get_value(iter, COLUMN_ONLINE):
            icon = 'network-offline'
        cell.set_property('icon-name', icon)

    def rows_reordered(self, model, path, iter, new_order):
        self.rebuild_config_file()

    def computer_toggled(self, cell, path, data):
        (model, column) = data
        model[path][column] = not model[path][column]
        self.rebuild_config_file()

    def on_main_window_delete_event(self, *args):
        self.mainWindow.hide()
        return True

    def close_clicked(self, button):
        self.mainWindow.hide()

    def show(self):
        if self.mainWindow:
            self.mainWindow.show_all()

    def rebuild_config_file(self):
        screensData = ''
        links = {}
        lastHost = None
        hostList = []
        for i in self.listStore:
            if i[COLUMN_ACTIVE]:
                host = i[COLUMN_HOST]
                screensData += '\n\t%s:' % host
                links[host] = {}
                hostList.append(host)
                if lastHost:
                    links[lastHost]['right'] = host
                    links[host]['left'] = lastHost
                lastHost = host
        linksData = ''
        for host, llist in links.items():
            linksData += '\n\t%s:' % host
            for dir, value in llist.items():
                linksData += '\n\t\t%s = %s' % (dir, value)
        configData = 'section: screens%s\nend\nsection: links%s\nend' % (screensData, linksData)
        name = os.path.join(os.environ["HOME"], '.synergy.conf')
        self.gconf.set_list(GCONF_KEY, gconf.VALUE_STRING, hostList)
        f = open(name, 'w')
        f.write(configData)
        f.close()
        self.parent.reload_synergy()

    def host_found(self, host, name, checked = False, online = True):
        friendlyName = host
        isLocal = (friendlyName == gethostname())
        if isLocal:
            friendlyName = '<b>%s</b> (this computer)' % friendlyName
        if self.discovered.has_key(host):
            iter = self.discovered[host]
            self.listStore.set(iter, COLUMN_ONLINE, online)
        else:
            iter = self.listStore.append((host, isLocal or checked, friendlyName, online))
            self.discovered[host] = iter
        self.nameToHost[name] = host

    def service_resolved(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags):
        '''
        Asynchronous callback when service data is resolved
        '''
        host = re.sub(r'\.%s$' % domain, '', host)
        self.host_found(host, name)

    def print_error(self, err):
        print "Error:", str(err)

    def new_service(self, interface, protocol, name, stype, domain, flags):
        '''
        Launched when a new service is found
        '''
        # resolve the new service
        self.server.ResolveService( int(interface), int(protocol), name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), reply_handler = self.service_resolved, error_handler = self.print_error)

    def name_disappeared(self, name):
        if self.nameToHost.has_key(name):
            host = self.nameToHost[name]
            iter = self.discovered[host]
            self.listStore.set(iter, COLUMN_ONLINE, False)

    def remove_service(self, interface, protocol, name, stype, domain, flags):
        '''
        Launched when a service disappears
        '''
        self.name_disappeared(name)
 
    def browse_domain(self, interface, protocol, domain):
        try:
            b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceBrowserNew(interface, protocol, '_workstation._tcp', domain, dbus.UInt32(0))),  avahi.DBUS_INTERFACE_SERVICE_BROWSER)
        except DBusException, e:
            print e
            error_msg(u'You should check that the avahi daemon is running.\n\nError: %s' % e)
            sys.exit(0)
        b.connect_to_signal('ItemNew', self.new_service)
        b.connect_to_signal('ItemRemove', self.remove_service)

STATE_RUNNING = 0
STATE_DIED = 1

class SynergyServer:
    pid = None
    serr = None
    state = STATE_DIED
    icon = None
    glade = None
    menu = None
    window = None
    clients = []
    server = None

    def set_state(self, state):
        if state == STATE_DIED:
            self.icon.set_from_icon_name('network-offline')
            self.icon.set_tooltip(u'Screen merging is not active')
        else:
            self.icon.set_from_icon_name('network-idle')
            self.icon.set_tooltip(u'Screen merging is active')
        self.state = state

    def notify_popup(self, title, body, icon = 'video-display', urgency = pynotify.URGENCY_LOW):
        notification = pynotify.Notification(title, body)
        notification.set_timeout(pynotify.EXPIRES_DEFAULT)
#         notification.set_property('status-icon', self.icon)
        notification.set_property('icon-name', icon)
        notification.set_urgency(urgency)
        return notification

    def avahi_announce(self):
        txt = ['os=linux']
        group = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP)
        group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), 'Synergy at %s' % gethostname(), '_synergy._tcp', '', '', dbus.UInt16(24800), avahi.string_array_to_txt_array(txt))
        group.Commit()

    def parse_synergy_output(self, line):
        clientMatch = re.search(r'client "(?P<client>[^"]+)" has connected', line)
        if clientMatch:
            host = clientMatch.group('client')
            notification = self.notify_popup(u'Screen connected', u'Computer "%s" is now sharing its screen.' % host)
            notification.show()
            self.clients.append(host)
        clientMatch = re.search(r'client "(?P<client>[^"]+)" has disconnected', line)
        if clientMatch:
            host = clientMatch.group('client')
            if host in self.clients:
                notification = self.notify_popup(u'Screen disconnected', u'Computer "%s" is no longer sharing its screen.' % host)
                notification.show()
                self.clients.remove(host)

    def synergy_output(self, fd, condition):
        if condition & gobject.IO_IN:
            data = os.read(fd, 4096)
            data = data.split('\n')
            for i in data:
                self.parse_synergy_output(i)
        if condition & gobject.IO_HUP:
            os.close(fd)
            return False
        else:
            return True

    def synergy_exited(self, pid, condition):
        self.set_state(STATE_DIED)
        self.pid = None
        notification = self.notify_popup(u'Screen merging is off', u'There is a problem with your current configuration', 'network-offline', pynotify.URGENCY_CRITICAL)
        notification.show()

    def reload_synergy(self):
        if self.state == STATE_DIED:
            self.start_synergy()
        else:
            os.kill(self.pid, signal.SIGHUP)

    def menu_called(self, icon, button, time):
        self.menu.popup(None, None, gtk.status_icon_position_menu, button, time, icon)

    def preferences_activate_cb(self, item):
        if self.window:
            self.window.show()
        else:
            self.window = MainWindow(self)

    def quit_activate_cb(self, item):
        if not self.state == STATE_DIED:
            os.kill(self.pid, signal.SIGTERM)
        gtk.main_quit()

    def start_synergy(self):
        (self.pid, sin, sout, self.serr) = gobject.spawn_async(['synergys', '-f', '-d', 'NOTE'], flags = gobject.SPAWN_SEARCH_PATH | gobject.SPAWN_DO_NOT_REAP_CHILD, standard_error = True)
        self.set_state(STATE_RUNNING)
        gobject.io_add_watch(self.serr, gobject.IO_IN | gobject.IO_HUP, self.synergy_output)
        gobject.child_watch_add(self.pid, self.synergy_exited)

    def __init__(self):
        self.bus = dbus.SystemBus()
        self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
        self.icon = gtk.StatusIcon()
        self.icon.connect('popup-menu', self.menu_called)
        self.reload_synergy()
        self.glade = gtk.glade.XML('synergys-popup.glade', 'main_menu', None)
        self.menu = self.glade.get_widget('main_menu')
        self.glade.signal_autoconnect(self)
        self.avahi_announce()

def main():
    some_stuff = SynergyServer()
    gtk.main()

if __name__ == "__main__":
    main()

